VUE.JS 双向绑定实现原理Object.defineProperty()

VUE.JS 双向绑定实现原理Object.defineProperty()

复习Object.defineProperty()

目前常见的几种 mvc (mvvm)框架 都实现了单向数据绑定。

双向数据绑定就是在单向绑定的基础上给可输入的元素添加change 事件,来动态的修改 model 和 view。

发布者-订阅者模式 (backbone.js)

脏值检查 (angular.js)

数据劫持(vue.js)

发布者-订阅者模式:一般通过 sub,pub 的方式实现数据和视图的监听绑定,更新数据方式通常做法是vm.set('property',value)

这种方式现在显得 low,我们希望通过 vm.property = value 这种方式更新数据,同时更新视图,于是有了下面两种方式

脏值检查:angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

数据劫持 : vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

要实现 mvvm 的双向绑定,必须

  1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
  2. 实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
  3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
  4. mvvm入口函数,整合以上三者

实现

1. 实现一个 Observer

Observer是一个数据监听器,其实现核心方法就是前文所说的Object.defineProperty( )。如果要对所有属性都进行监听的话,那么可以通过递归方法遍历所有属性值,并对其进行Object.defineProperty( )处理。如下代码,实现了一个Observer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function defineReactive(data, key, val) {
observe(val); // 递归遍历所有子属性(这边不太明白)
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
return val;
},
set: function(newVal) {
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
}
});
}

function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
};

var library = {
book1: {
name: ''
},
book2: ''
};
observe(library);
library.book1.name = 'vue权威指南'; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = '没有此书籍'; // 属性book2已经被监听了,现在值为:“没有此书籍”

思路分析中,需要创建一个可以容纳订阅者的消息订阅器Dep,订阅器Dep主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数。所以显然订阅器需要有一个容器,这个容器就是list,将上面的Observer稍微改造下,植入消息订阅器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//link  这边不太明白
function defineReactive(data, key, val) {
observe(val); // 递归遍历所有子属性
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
if (是否需要添加订阅者) {
dep.addSub(watcher); // 在这里添加一个订阅者
}
return val;
},
set: function(newVal) {
if (val === newVal) {
return;
}
val = newVal;
console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
dep.notify(); // 如果数据变化,通知所有订阅者
}
});
}

function Dep () {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};

从代码上看,我们将订阅器Dep添加一个订阅者设计在getter里面,这是为了让Watcher初始化进行触发,因此需要判断是否要添加订阅者,至于具体设计方案,下文会详细说明的。在setter函数里面,如果数据变化,就会去通知所有订阅者,订阅者们就会去执行对应的更新的函数。到此为止,一个比较完整Observer已经实现了,接下来我们开始设计Watcher。

之后有点不明白

学习链接 vue数据双向绑定原理

剖析Vue原理&实现双向绑定MVVM

2.实现 watcher

补充:Object.defineProperty()

1
2
3
4
5
6
7
8
9
//数据描述
Object.defineProperty(obj,"key",{
enumerable:false, // 可否 delete 目标属性是否可以再次设置特性 默认 false
configurable:false, //可否枚举 使用for...in或Object.keys() 默认 false
writable:false, // 是否被赋值运算符改变(重写) 默认 false
value:"static" //值 默认 undefined
})
//obj:必需。目标对象
//prop:必需。需定义或修改的属性的名字(key)
1
2
3
4
5
6
7
8
9
10
11
12
13
//存取器描述
var obj = {};
Object.defineProperty(obj,"newKey",{
get:function (){} | undefined,
set:function (value){} | undefined
configurable: true | false
enumerable: true | false
});
//注意:当使用了getter或setter方法,不允许使用writable和value这两个属性

//注意:get或set不是必须成对出现,任写其一就可以。如果不设置方法,则get和set的默认值为undefined

//configurable和enumerable同上面的用法。

在ie8下只能在DOM对象上使用,尝试在原生的对象使用 Object.defineProperty()会报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<input type="text" id="a" h-model="text">
{{text}}
</div>

<script type="text/javascript">
//和dom有关 数据绑定初始化
function convertNode(node, vm) {
var fragment = document.createDocumentFragment(),child;
while(child = node.firstChild){
compile(child,vm)
fragment.appendChild(child)
}
return fragment
}
function compile(node,vm) {
var reg = /\{\{(.*)\}\}/;
if(node.nodeType===1){ //节点类型为元素节点
var attr = node.attributes; //对所有属性进行解析
for(var i = 0;i<attr.length;i++){
if(attr[i].nodeName == 'h-model'){ //匹配h-model
//将元素与数据绑定
var bindName = attr[i].nodeValue;
node.value = vm.data[bindName]

//为输入框添加事件监听触发
node.addEventListener('input',function (e) {
vm.data[bindName] = e.target.value;
node.value = vm.data[bindName];
})

node.removeAttribute('h-model');
}
}
}
if(node.nodeType===3){
if(reg.test(node.nodeValue)){
var bindName = RegExp.$1.trim()
// console.log(RegExp.$1)
// node.nodeValue = vm.data[bindName]
new Watcher(vm,node,bindName)
}
}
}

//获取数据相关 数据响应
function Observer(data, vm) {
// console.log(data)
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val) {
Observer(val); // 递归遍历所有子属性
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
// console.log(Dep.target)
if(Dep.target){ //Dep.target存在的话,将目标元素添加到当前data属性的观察者列表中
dep.addSub(Dep.target);
}
return val;
},
set: function(newVal) {
if(val === newVal) return;
val = newVal;
dep.notify(); // 如果数据变化,通知所有订阅者
// console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
}
});
}

function Dep() {
this.subs=[];
}
Dep.prototype = {
addSub:function (sub) { //被观察者列表的添加动作
this.subs.push(sub);
},
notify: function () { //对观察者列表的所有观察者触发更新
this.subs.forEach(function (value) {
console.log(value)
value.update();
})
}
}
function Watcher(vm, node, bindname) {
//将全局dep.target设置为当前页面元素node
Dep.target = this;
//完成watcher 的初始化
this.name = bindname;
this.node = node;
this.vm = vm;

this.update(); //初次绑定时进行更新
Dep.target = null;
}
Watcher.prototype = {
get:function () {
this.value = this.vm.data[this.name]
},
update: function () {
this.get();
this.node.nodeValue = this.value;
}
}

function Vue(options){
this.data = options.data
Observer(this.data,this)
var id = options.el;
var dom = convertNode(document.getElementById(id),this)
document.getElementById(id).appendChild(dom)
}
var vm = new Vue({
el:'app',
data:{
text:'Hello MVVM',
aaa:{
text:"hello"
}
}
});
</script>
</body>
</html>
感谢你的打赏哦!